「翻译」使用Jest和Enzyme进行React测试:入门

原文链接

前言

随着 React 应用变得越来越复杂,其稳定性也会随之变差。无论是抽象、添加功能或者是重构,都可能会引起 bug,更别说多名开发者共同维护项目的情况了。所以,我们就需要自动化测试的帮助。

本文将会介绍自动化测试工具、我们可以在 React 中进行哪些测试以及为什么需要这么做。让我们首先来介绍 Jest,这是最常用的由 FB 开发的JS 测试框架,FB 也是用它来给 React 应用进行测试。

介绍 Jest

首先介绍的工具是 Jest。Jest 是一个基于 Node 的测试工具,快速并行运行。它可以使用在任何由 CRA 创建的项目。在你的 app 目录下,在终端运行 npm test ,就会初始化 Jest,然后开始在观察模式下进行测试。这意味着在你开发的时候,任何文件的改动都会引起重新测试。

如果不是通过 CRA 创建的项目呢?

如果没用 CRA,那么只需要通过 npm 或者 yarn 来安装 Jest。

1
2
3
yarn add --dev jest
#or
npm install --save-dev jest

为了能通过 npm test 来运行 jest,在你的 package.json 文件的 scripts 块中添加一个 test

1
2
3
4
5
...
scripts: {
"test": "jest",
...
}

不过,在 CRA 环境下长得有点不同,它使用 react-scripts 来代替 jest

1
2
3
4
5
...
scripts: {
"test": "react-scripts test",
...
}

试试在你的终端运行 npm test 。你现在就会处于观察模式,可以看到一句话:「No tests found related to files changed since last commit.」,后面跟着一些快捷键来运行不同方式的测试。

那么,在你的 React 项目里引入测试的最佳实践是什么呢?在介绍更多有用的工具前,我们先试试这个。

开始使用 Jest

在你的项目里,Jest 通过文件名来找到测试文件,不论他们被藏得有多深。这里有 3 种命名方式:

  • 任何以 .test.js 或者 .spec.js 作为扩展名的文件。它一般就放在组件文件旁边,比如 App.js 旁边放一个 App.test.js 。它的好处就是查找和引用声明比较方便。
  • 任何在 __tests__ 文件夹里的 .js 文件。如果你需要用多个测试文件来测试一个特定的组件或组件文件夹,那么一个 __tests__ 文件夹就能提供一个更连贯的结构,使你的测试和组件文件不至于混在一起。这个更适合大型项目。

使用哪种方式都随你,也取决于你的项目情况。

现在让我们来写一个简单的测试来学学 Jest 的语法。让我们在 App.js 旁边添加一个 App.test.js 文件。这个测试现在还不会跟 App.js 相关,但会引入一些 Jest 的关键方法:describe()it()expect()

App.test.js:

1
2
3
4
5
6
7
describe('Examining the syntax of Jest tests', () => {

it('sums numbers', () => {
expect(1 + 2).toEqual(3);
expect(2 + 2).toEqual(4);
});
});

上面第一眼看过去,你会发现这是个显而易见的测试,非常好。对测试来说,易读性非常重要。

现在,保存一下,如果你已经跑起来了 Jest,那么就会收到一个类似于这样的信息:

现在我们来理解一下上面这个例子里使用的语法:

  • describe() : 这是一个可选方法,它可以用来包裹一组测试,让我们通过文字来描述它所包裹的这组测试的行为。正如截图所示,这些文字出现在了测试结果的头部。
  • it() : 在本质上和 describe() 是类似的。it() 让我们通过文字来描述一个测试需要成功实现什么。你可能会发现在 Jest documentation 中使用的是 test() 而不是 it() 。这两个都是有效的。
  • expect().toEqual() : 这里我们进行的是测试本身。expect() 方法包裹了一个函数的执行结果,toEqual() 则包裹了一个应该和 expect() 一致的值。

expect() 创造了一个「断言」:什么是「断言」?

expect() 是一个全局的 Jest 函数,用来创建「断言」。断言是一个返回布尔值的函数,并且我们希望它返回的是 true —— 因此称为 expect。如果返回了 false,那么就代表这个测试没通过,同时 it() 或者 test() 块里的执行就会停止。


toEqual() 是一个匹配器:什么是匹配器?

toEqual() 被称为「匹配器」。匹配器是一个返回值必须和 expect() 预期结果一致的函数。Jest 的文档里列举了全部的匹配器(链接 ),包括 toContaintoBeFalsytoMatch(regexpOrString)toThrow(error)。去了解哪些可以优化你的测试语法是值得的。


我们在终端得到每个测试的结果。绿色代表成功,红色代表失败。

为了得到一个失败结果(希望不要有!),我们来改成 expect(2 + 2).toEqual(5)

我们的终端显示了一些有用信息,包括挂在哪个文件,第几行,什么代码。

为什么要测试 2 个甚至多个加法,而不是一个就够了?

我们的第一个例子测试了2次加法,一次是 1 + 2,另一次是 2 + 2。这非常重要。为了解释原因,现在我们自己来实现一个加法:

./math.js:

1
export const sum = (x, y) => x + y;

./App.test.js:

1
2
3
4
5
6
7
8
import {sum} from './math';
describe('Examining the syntax of Jest tests', () => {

it('sums numbers', () => {
expect(sum(1, 2)).toEqual(3);
expect(sum(2, 2)).toEqual(4);
});
});

当然,现在还是会成功通过测试的。但假如我们把 sum() 做一点改动:

1
export const sum = (x, y) => 3;

我们把 sum 函数的返回值固定为 3。现在,加入我们只测试 1 + 2 的和,这个测试就会顺利通过。但这并不是我们想要的,它只返回 3!

为什么呢?在开发的时候,错误就会出现。我可能想要测试一个获取请求方法,当时写死了返回值,之后就忘记切换回动态调用了。我也可能在编写大量数据库调用方法的时候,忘记在占位符里插入查询语句。对于任何情况来说,使用 it()test() 对每个函数或组件执行多种断言总是非常有用的。

以上,我们测试了普通 JS 函数。现在,来试试如何测试 React 组件吧。为了实现这个需求,我们需要引入另一个配合 Jest 的包:Enzyme

使用 Enzyme 来测试 React 组件

Enzyme 是一个测试工具包,由 Airbnb 开发,它让测试 React 组件变得更加方便。React 官方确实有提供内置测试工具:testing suite ,但这些工具又冗长又粗糙,反而是 Enzyme 提供了更优雅的方法来执行我们的测试。

与 Jest 不同的是,Enzyme 并没有被 CRA 引入。安装 Enzyme 方法如下:

1
2
3
yarn add --dev enzyme enzyme-adapter-react-16 react-test-renderer
#or
npm install --save-dev enzyme enzyme-adapter-react-16 react-test-renderer

安装的适配器需要跟随使用的 React 版本。这里我们是以 React 16 为例,当然早期版本也有提供( 链接 )。

我们需要一个 src/setupTests.js 文件来使用 Enzyme,它指定 Enzyme 运行我们刚刚安装的适配器。保证这个文件在使用 Enzyme 前被引入。

src/setupTest.js:

1
2
3
4
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

configure({ adapter: new Adapter() });

现在,我们就可以开始使用 Enzyme 来进行测试了。举个例子,如果我们想要测试 <App /> 组件,那么就可以在 App.test.js 文件中加入:

1
2
3
4
5
6
7
8
9
import React from 'react';
import { shallow } from 'enzyme';
import App from './App';

describe('First React component test with Enzyme', () => {
it('renders without crashing', () => {
shallow(<App />);
});
});

这里呢我们使用了 shallow() 断言方法。shallow() 会测试我们提供的组件,并且忽略任何它的子组件。如果我们有个 <Header /><Footer /><App /> 里,他们都会在测试中被忽略。

shallow() 属于单元测试。单元测试指的是只测试一个函数的测试。当我们测试的函数依赖于其他函数的时候,就成为了集成测试。同样的,如果我们测试一整个 React 组件树,那么就变成了一个集成测试。

在 Enzyme 中,我们可以通过 mount() 来实现这样的测试。它是一种完整渲染的方式,把整个组件树和生命周期考虑进去。

Jest 如何在一个 DOM 环境里测试?

我们知道,Jest 运行在 node 环境里,但 React 组件需要在 DOM 环境里进行测试。那么怎么办呢?使用 jsdom 。这是一个 WHATWG DOM 和 HTML 标准的 JS 实践。

jsdom 在我们的 CRA 测试环境里已经默认开启了,因此,我们可以在测试的时候,使用包括 windowdocument 等全局变量来模仿浏览器行为。虽然 jsdom 允许我们为组件进行单元和集成测试,但不推荐进行端到端的浏览器测试。还是要跑起一个真实的浏览器驱动去看结果。

你可以在 package.json 的命令里使用 --env=node 标识来关闭 jsdom,但如果要进行集成测试的话就不适合了。因为需要有真实渲染。

CRA 文档里高亮了你可以关闭 jsdom 的情况( 链接 )。关闭它可以让你的测试跑得更快。

测试术语

现在是时候来看看 React 测试的主要术语了。先来看一些常用的:

  • 单元测试:测试独立的函数,或一个 React 组件。Enzyme 的 shallow() 方法是单元测试。
  • 集成测试:测试多个函数一起工作,或者一整个 React 组件树。Enzyme 的 mount() 方法是集成测试。
  • Mock 函数:重定义一个函数,特别是在测试的时候生成一个结果。比如返回定值,而不是依赖于获取请求或数据库调用。它可以防止我们之前发现的那个返回定值的求和问题。Mock 函数可以定义成: jest.fn(() => { // function here})

测试术语:

  • 冒烟测试:验证一个组件正常渲染。shallow()mount() 都是。
  • 浅渲染:渲染组件不带子组件。shallow() 是。
  • 完整渲染:渲染完整树。mount() 是。
  • 静态测试:渲染组件到静态 HTML 文件,然后分析结果。Enzyme 的 render() 方法用来静态测试。